Pulled in the twitter content backup functionality from curvyandtrans.com
Sadly, lost the images from one of Emmy_Zje's deleted tweets.
@ -12,16 +12,17 @@ const actions = {
|
|||||||
return readFile(input);
|
return readFile(input);
|
||||||
},
|
},
|
||||||
|
|
||||||
async transcode ({ input, output }) {
|
async transcode ({ input, output, duplicate }) {
|
||||||
const result = await actions.image({
|
const result = await actions.image({
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
duplicate,
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch ({ input, output }) {
|
async fetch ({ input, output, duplicate }) {
|
||||||
const res = await fetch(input);
|
const res = await fetch(input);
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error(`File could not be fetched (${res.status}): "${input}"`);
|
throw new Error(`File could not be fetched (${res.status}): "${input}"`);
|
||||||
@ -30,16 +31,24 @@ const actions = {
|
|||||||
output = resolve(output);
|
output = resolve(output);
|
||||||
await fs.ensureDir(path.dirname(output));
|
await fs.ensureDir(path.dirname(output));
|
||||||
await fs.writeFile(output, body);
|
await fs.writeFile(output, body);
|
||||||
|
if (duplicate) {
|
||||||
|
await fs.ensureDir(resolve(path.dirname(duplicate)));
|
||||||
|
await fs.writeFile(duplicate, body);
|
||||||
|
}
|
||||||
return body;
|
return body;
|
||||||
},
|
},
|
||||||
|
|
||||||
async write ({ output, content }) {
|
async write ({ output, content, duplicate }) {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new TypeError('Got an empty write?' + output);
|
throw new TypeError('Got an empty write?' + output);
|
||||||
}
|
}
|
||||||
output = resolve(output);
|
output = resolve(output);
|
||||||
await fs.ensureDir(path.dirname(output));
|
await fs.ensureDir(path.dirname(output));
|
||||||
await fs.writeFile(output, content);
|
await fs.writeFile(output, content);
|
||||||
|
if (duplicate) {
|
||||||
|
await fs.ensureDir(resolve(path.dirname(duplicate)));
|
||||||
|
await fs.writeFile(duplicate, content);
|
||||||
|
}
|
||||||
return Buffer.from(content);
|
return Buffer.from(content);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -157,6 +166,10 @@ const actions = {
|
|||||||
let result = await Promise.fromCallback((cb) => gmfile.toBuffer(cb));
|
let result = await Promise.fromCallback((cb) => gmfile.toBuffer(cb));
|
||||||
if (options.format === 'ico') result = await ico(result);
|
if (options.format === 'ico') result = await ico(result);
|
||||||
await fs.writeFile(output, result);
|
await fs.writeFile(output, result);
|
||||||
|
if (options.duplicate) {
|
||||||
|
await fs.ensureDir(resolve(path.dirname(options.duplicate)));
|
||||||
|
await fs.writeFile(options.duplicate, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -72,10 +72,10 @@ module.exports = exports = class Manifest {
|
|||||||
|
|
||||||
async get (task) {
|
async get (task) {
|
||||||
const hash = this.hash(task);
|
const hash = this.hash(task);
|
||||||
const { input, output } = task;
|
const { input, output, cache: altCachePath } = task;
|
||||||
const ext = path.extname(task.output);
|
const ext = path.extname(task.output);
|
||||||
const local = !task.input.includes('://');
|
const local = !task.input.includes('://');
|
||||||
const cached = path.join(CACHE, hash + ext);
|
var cached = path.join(CACHE, hash + ext);
|
||||||
const result = {
|
const result = {
|
||||||
iTime: 0,
|
iTime: 0,
|
||||||
iRev: null,
|
iRev: null,
|
||||||
@ -85,9 +85,15 @@ module.exports = exports = class Manifest {
|
|||||||
action: task.action.name,
|
action: task.action.name,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
duplicate: altCachePath,
|
||||||
mode: 'new',
|
mode: 'new',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var acTime;
|
||||||
|
if (altCachePath) {
|
||||||
|
acTime = await this.stat(altCachePath);
|
||||||
|
}
|
||||||
|
|
||||||
const [ iTime, oTime, cTime, iRev ] = await Promise.all([
|
const [ iTime, oTime, cTime, iRev ] = await Promise.all([
|
||||||
local && this.stat(input),
|
local && this.stat(input),
|
||||||
this.stat(output),
|
this.stat(output),
|
||||||
@ -148,7 +154,17 @@ module.exports = exports = class Manifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.mode = 'cached';
|
result.mode = 'cached';
|
||||||
result.cache = await readFile(cached);
|
|
||||||
|
if (acTime && acTime > cTime) {
|
||||||
|
result.cache = await readFile(altCachePath);
|
||||||
|
} else {
|
||||||
|
result.cache = await readFile(cached);
|
||||||
|
if (altCachePath && !acTime) {
|
||||||
|
await fs.ensureDir(resolve(path.dirname(altCachePath)));
|
||||||
|
await fs.writeFile(resolve(altCachePath), result.cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +173,7 @@ module.exports = exports = class Manifest {
|
|||||||
if (task.nocache || !task.action.name) return null;
|
if (task.nocache || !task.action.name) return null;
|
||||||
|
|
||||||
const hash = this.hash(task);
|
const hash = this.hash(task);
|
||||||
const { input, output } = task;
|
const { input, output, cache: altCachePath } = task;
|
||||||
const local = !task.input.includes('://');
|
const local = !task.input.includes('://');
|
||||||
|
|
||||||
const [ iTime, iRev ] = await Promise.all([
|
const [ iTime, iRev ] = await Promise.all([
|
||||||
@ -175,6 +191,7 @@ module.exports = exports = class Manifest {
|
|||||||
output,
|
output,
|
||||||
oTime: Math.floor(lastSeen / 1000),
|
oTime: Math.floor(lastSeen / 1000),
|
||||||
lastSeen,
|
lastSeen,
|
||||||
|
duplicate: altCachePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (record.revPath) this.revManifest[output] = record.revPath;
|
if (record.revPath) this.revManifest[output] = record.revPath;
|
||||||
@ -185,17 +202,22 @@ module.exports = exports = class Manifest {
|
|||||||
|
|
||||||
async set (task, result, lastSeen = new Date()) {
|
async set (task, result, lastSeen = new Date()) {
|
||||||
const hash = this.hash(task);
|
const hash = this.hash(task);
|
||||||
const { input, output } = task;
|
const { input, output, cache: altCachePath } = task;
|
||||||
const nocache = task.nocache || task.action.name === 'copy';
|
const nocache = task.nocache || task.action.name === 'copy';
|
||||||
const ext = path.extname(task.output);
|
const ext = path.extname(task.output);
|
||||||
const local = !task.input.includes('://');
|
const local = !task.input.includes('://');
|
||||||
const cached = path.join(CACHE, hash + ext);
|
const cached = path.join(CACHE, hash + ext);
|
||||||
const oRev = revHash(result);
|
const oRev = revHash(result);
|
||||||
|
|
||||||
|
if (result && altCachePath) {
|
||||||
|
await fs.ensureDir(resolve(path.dirname(altCachePath)));
|
||||||
|
}
|
||||||
|
|
||||||
const [ iTime, iRev ] = await Promise.all([
|
const [ iTime, iRev ] = await Promise.all([
|
||||||
local && this.stat(input),
|
local && this.stat(input),
|
||||||
local && this.compareBy.inputRev && this.revFile(input),
|
local && this.compareBy.inputRev && this.revFile(input),
|
||||||
result && !nocache && fs.writeFile(resolve(cached), result),
|
result && !nocache && fs.writeFile(resolve(cached), result),
|
||||||
|
result && altCachePath && fs.writeFile(resolve(altCachePath), result),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const record = {
|
const record = {
|
||||||
@ -208,6 +230,7 @@ module.exports = exports = class Manifest {
|
|||||||
oTime: Math.floor(lastSeen / 1000),
|
oTime: Math.floor(lastSeen / 1000),
|
||||||
oRev,
|
oRev,
|
||||||
lastSeen,
|
lastSeen,
|
||||||
|
duplicate: altCachePath,
|
||||||
revPath: revPath(output, oRev),
|
revPath: revPath(output, oRev),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
var twemoji = require('twemoji' );
|
var twemoji = require('twemoji' );
|
||||||
const { deepPick, has } = require('./util');
|
const { deepPick, has } = require('./util');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
id_str: true,
|
id_str: true,
|
||||||
@ -23,6 +24,7 @@ const schema = {
|
|||||||
} ] },
|
} ] },
|
||||||
} ] },
|
} ] },
|
||||||
media: true,
|
media: true,
|
||||||
|
in_reply_to_status_id_str: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var entityProcessors = {
|
var entityProcessors = {
|
||||||
@ -94,7 +96,8 @@ module.exports = exports = function (tweets) {
|
|||||||
|
|
||||||
tweet.user.avatar = {
|
tweet.user.avatar = {
|
||||||
input: tweet.user.profile_image_url_https,
|
input: tweet.user.profile_image_url_https,
|
||||||
output: 'tweets/' + tweet.user.screen_name + '.jpg',
|
output: `tweets/${tweet.user.screen_name}.jpg`,
|
||||||
|
cache: `twitter-avatars/${tweet.user.screen_name}.jpg`,
|
||||||
};
|
};
|
||||||
|
|
||||||
tweet.media = [
|
tweet.media = [
|
||||||
@ -113,7 +116,22 @@ module.exports = exports = function (tweets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (has(tweet, 'entities.media') && has(tweet, 'extended_entities.media')) {
|
if (has(tweet, 'entities.media') && has(tweet, 'extended_entities.media')) {
|
||||||
tweet.entities.media = tweet.extended_entities.media;
|
tweet.entities.media = tweet.extended_entities.media.map((media) => {
|
||||||
|
media = { ...media };
|
||||||
|
if (media.media_url_https) {
|
||||||
|
const mediaItem = {
|
||||||
|
input: media.media_url_https,
|
||||||
|
output: `tweets/${tweet.id_str}/${path.basename(media.media_url_https)}`,
|
||||||
|
cache: `twitter-entities/${tweet.id_str}/${path.basename(media.media_url_https)}`,
|
||||||
|
};
|
||||||
|
if (media.type === 'photo') mediaItem.input += '?name=medium';
|
||||||
|
tweet.media.push(mediaItem);
|
||||||
|
media.media_url_https = '/' + mediaItem.output;
|
||||||
|
}
|
||||||
|
|
||||||
|
return media;
|
||||||
|
});
|
||||||
|
|
||||||
delete tweet.extended_entities;
|
delete tweet.extended_entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { chunk, uniq, difference } = require('lodash');
|
const { chunk, uniq, uniqBy, difference } = require('lodash');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { resolve } = require('./resolve');
|
const { resolve } = require('./resolve');
|
||||||
const log = require('fancy-log');
|
const log = require('fancy-log');
|
||||||
@ -65,7 +65,7 @@ module.exports = exports = async function tweets (pages) {
|
|||||||
|
|
||||||
/* Apply Tweets to Pages **************************************************/
|
/* Apply Tweets to Pages **************************************************/
|
||||||
|
|
||||||
const twitterMedia = [];
|
var twitterMedia = [];
|
||||||
|
|
||||||
function attachTweet (dict, tweetid) {
|
function attachTweet (dict, tweetid) {
|
||||||
if (!hasOwn(twitterCache, tweetid) && twitterBackup[tweetid]) {
|
if (!hasOwn(twitterCache, tweetid) && twitterBackup[tweetid]) {
|
||||||
@ -91,6 +91,8 @@ module.exports = exports = async function tweets (pages) {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
twitterMedia = uniqBy(twitterMedia, 'output');
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fs.writeFile(resolve('twitter-media.json'), JSON.stringify(twitterMedia, null, 2)),
|
fs.writeFile(resolve('twitter-media.json'), JSON.stringify(twitterMedia, null, 2)),
|
||||||
fs.writeFile(resolve('twitter-cache.json'), JSON.stringify(twitterCache, null, 2)),
|
fs.writeFile(resolve('twitter-cache.json'), JSON.stringify(twitterCache, null, 2)),
|
||||||
|
@ -94,7 +94,7 @@ const TYPE = exports.TYPE = {
|
|||||||
|
|
||||||
exports.type = dictMatch({
|
exports.type = dictMatch({
|
||||||
[TYPE.IMAGE]: isImage,
|
[TYPE.IMAGE]: isImage,
|
||||||
[TYPE.HANDYBARS]: isHandybars,
|
[TYPE.HANDYBARS]: isHandybars,
|
||||||
[TYPE.MARKDOWN]: isMarkdown,
|
[TYPE.MARKDOWN]: isMarkdown,
|
||||||
[TYPE.VIDEO]: isVideo,
|
[TYPE.VIDEO]: isVideo,
|
||||||
[TYPE.SCRIPT]: is(JS, JSX),
|
[TYPE.SCRIPT]: is(JS, JSX),
|
||||||
|
107
build/twitter-client.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const { resolve } = require('./resolve');
|
||||||
|
const { chunk, uniq, difference } = require('lodash');
|
||||||
|
const Twitter = require('twitter-lite');
|
||||||
|
const log = require('fancy-log');
|
||||||
|
const tweetparse = require('./lib/tweetparse');
|
||||||
|
const { hasOwn } = require('./lib/util');
|
||||||
|
|
||||||
|
module.exports = exports = async function () {
|
||||||
|
const tc = new TwitterClient();
|
||||||
|
await tc.initialize();
|
||||||
|
return tc;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class TwitterClient {
|
||||||
|
|
||||||
|
async initialize () {
|
||||||
|
const [ lookup, backup, cache ] = await Promise.all([
|
||||||
|
fs.readJson(resolve('twitter-config.json')).catch(() => null)
|
||||||
|
.then(makeFetcher),
|
||||||
|
fs.readJson(resolve('twitter-backup.json')).catch(() => ({})),
|
||||||
|
fs.readJson(resolve('twitter-cache.json')).catch(() => ({})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this._lookup = lookup;
|
||||||
|
this._backupData = backup;
|
||||||
|
this._cache = cache;
|
||||||
|
this._presentTweets = Object.keys(cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
async write () {
|
||||||
|
return Promise.all([
|
||||||
|
fs.writeFile(resolve('twitter-cache.json'), JSON.stringify(this._cache, null, 2)),
|
||||||
|
fs.writeFile(resolve('twitter-backup.json'), JSON.stringify(this._backupData, null, 2)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get (tweetids) {
|
||||||
|
if (!Array.isArray(tweetids)) tweetids = [ tweetids ];
|
||||||
|
|
||||||
|
tweetids = uniq(tweetids.map(parseTweetId));
|
||||||
|
|
||||||
|
let tweetsNeeded = this._missing(tweetids);
|
||||||
|
|
||||||
|
while (tweetsNeeded.length) {
|
||||||
|
log('Fetching tweets: ' + tweetsNeeded.join(', '));
|
||||||
|
const arriving = await Promise.all(chunk(tweetsNeeded, 99).map(this._lookup));
|
||||||
|
const tweetsRequested = tweetsNeeded;
|
||||||
|
tweetsNeeded = [];
|
||||||
|
const loaded = [];
|
||||||
|
for (const tweet of arriving.flat(1)) {
|
||||||
|
if (tweet.quoted_status_id_str && !this._cache[tweet.quoted_status_id_str]) {
|
||||||
|
tweetsNeeded.push(tweet.quoted_status_id_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._backupData[tweet.id_str] = tweet;
|
||||||
|
this._cache[tweet.id_str] = tweetparse(tweet);
|
||||||
|
loaded.push(tweet.id_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
const absent = difference(tweetsRequested, loaded);
|
||||||
|
for (const id of absent) {
|
||||||
|
if (!hasOwn(this._backupData, id)) {
|
||||||
|
log.error('Could not find tweet ' + id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tweet = this._backupData[id];
|
||||||
|
|
||||||
|
if (tweet) {
|
||||||
|
log('Pulled tweet from backup ' + id);
|
||||||
|
this._cache[id] = tweetparse(this._backupData[id]);
|
||||||
|
} else {
|
||||||
|
this._cache[id] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tweetids.map((id) => this._cache[id] || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_missing (tweetids) {
|
||||||
|
return difference(tweetids, this._presentTweets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function makeFetcher (config) {
|
||||||
|
if (!config) return () => [];
|
||||||
|
const client = new Twitter(config);
|
||||||
|
return (tweetids) => client
|
||||||
|
.get('statuses/lookup', { id: tweetids.join(','), tweet_mode: 'extended' })
|
||||||
|
// .then((r) => { console.log({r}); return r; })
|
||||||
|
.catch((e) => { log.error(e); return []; });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tweeturl = /https?:\/\/twitter\.com\/(?:#!\/)?(?:\w+)\/status(?:es)?\/(\d+)/i;
|
||||||
|
const tweetidcheck = /^\d+$/;
|
||||||
|
function parseTweetId (tweetid) {
|
||||||
|
// we can't trust an id that isn't a string
|
||||||
|
if (typeof tweetid !== 'string') return false;
|
||||||
|
|
||||||
|
const match = tweetid.match(tweeturl);
|
||||||
|
if (match) return match[1];
|
||||||
|
if (tweetid.match(tweetidcheck)) return tweetid;
|
||||||
|
return false;
|
||||||
|
}
|
34
build/twitter-thread.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
const twitterClient = require('./twitter-client');
|
||||||
|
|
||||||
|
module.exports = exports = async function loadThread (tweetid) {
|
||||||
|
const tc = await twitterClient();
|
||||||
|
|
||||||
|
async function quoteds (tweet) {
|
||||||
|
if (!tweet.quoted_status_id_str) return [];
|
||||||
|
const [ qt ] = await tc.get(tweet.quoted_status_id_str);
|
||||||
|
if (!qt) return [];
|
||||||
|
return [ qt.id_str, ...(await quoteds(qt)) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeds = [];
|
||||||
|
const dependencies = [];
|
||||||
|
let id = tweetid;
|
||||||
|
do {
|
||||||
|
const [ tweet ] = await tc.get(id);
|
||||||
|
if (!tweet) break;
|
||||||
|
embeds.unshift(tweet.id_str);
|
||||||
|
dependencies.unshift(tweet.id_str);
|
||||||
|
|
||||||
|
if (tweet.quoted_status_id_str) {
|
||||||
|
const qts = await quoteds(tweet);
|
||||||
|
if (qts.length) dependencies.unshift(...qts);
|
||||||
|
}
|
||||||
|
|
||||||
|
id = tweet.in_reply_to_status_id_str;
|
||||||
|
} while (id);
|
||||||
|
|
||||||
|
await tc.write();
|
||||||
|
|
||||||
|
return [ embeds, dependencies ];
|
||||||
|
};
|
BIN
twitter-avatars/ABC.jpg
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
twitter-avatars/AFortune69.jpg
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
twitter-avatars/AOC.jpg
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
twitter-avatars/Adoratrix.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/Amsteffy87.jpg
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
twitter-avatars/BathysphereHat.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/BellaRizinti.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/BonzoixofMN.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/CateSpice.jpg
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
twitter-avatars/Chican3ry.jpg
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
twitter-avatars/CognitiveSoc.jpg
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
twitter-avatars/CrypticGoblin.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/CuddlePotato.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/DameKraft.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/DrRachaelF.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/EloraEdwards.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/Emmy_Zje.jpg
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
twitter-avatars/ErinInTheMorn.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/GenderGP.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/KaliRainH2O.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/KatyMontgomerie.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/LisaTMullin.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/MagsVisaggs.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/MoonyXIV.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/NMorganCreates.jpg
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
twitter-avatars/NightlingBug.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/OneWeirdAngel.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/OttoLontra.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/QuinnPBrown.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/RaeGun2k.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/RebeccaRHelm.jpg
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
twitter-avatars/RileyFaelan.jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
twitter-avatars/RoseOfWindsong.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/RowanChurch.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/S_Winterthorn.jpg
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
twitter-avatars/SassKnight.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/SecretGamerGrrl.jpg
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
twitter-avatars/ShaggyDemiurge.jpg
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
twitter-avatars/TheEmmelineMay.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/TransEthics.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/TransSalamander.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/Tuplet.jpg
Normal file
After Width: | Height: | Size: 769 B |
BIN
twitter-avatars/TwippingVanilla.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/alexand_erleon.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/alicemiriel.jpg
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
twitter-avatars/ayumeril.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/biscuit_heid.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/blotchkat.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/buttonsandfluff.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/bzarcher.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/canamKim.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/cismender.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/crypticenbug.jpg
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
twitter-avatars/delaneykingrox.jpg
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
twitter-avatars/itsaddis.jpg
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
twitter-avatars/meimeimeixie.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/naylorillo.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/persenche.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/queenofnevers.jpg
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
twitter-avatars/raddifferent.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/rozietoez.jpg
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
twitter-avatars/snakmicks.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/tedcruz.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/tomorrowville.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
twitter-avatars/two_n_minus_one.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
twitter-avatars/zeghostboy.jpg
Normal file
After Width: | Height: | Size: 1.9 KiB |
12443
twitter-backup.json
BIN
twitter-entities/1025590255733366784/Djugv7jWwAAGVVM.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
twitter-entities/1025590255733366784/Djugv7jXcAA8scb.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
twitter-entities/1025590255733366784/Djugv7lW0AAq52Z.jpg
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
twitter-entities/1046808859074080768/DocC-j8XgAgXutl.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
twitter-entities/1092180584653381632/Dyg0IXMXQAAgrig.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
twitter-entities/1094677550486310917/DzETBhQUYAAWJSc.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
twitter-entities/1094678128335568896/DzETp3jU0AA1F26.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
twitter-entities/1094683008919957504/DzEYN-7VYAAgq1Y.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
twitter-entities/1094687092871880705/DzEbX1AUwAA157v.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
twitter-entities/1094717735974490112/DzE38mUUcAAAAWN.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
twitter-entities/1094730604501848064/DzFDbTwV4AAFFFh.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
twitter-entities/1162051481656266760/ECBvqZnW4Agg00A.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
twitter-entities/1214536380501524481/ENrmY-hU8AET_25.jpg
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
twitter-entities/1228717614630940672/EQ1IKINWkAAllKR.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
twitter-entities/1232817112139341824/rY9vCIuJrqOn2kiH.jpg
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
twitter-entities/1235235759721979906/ESRuPAHWoAA0WVi.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
twitter-entities/1235771137164009474/ESZXRt_X0AAbHrS.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
twitter-entities/1286504315960438784/EdqUVvHUMAE-0jk.png
Normal file
After Width: | Height: | Size: 299 KiB |
BIN
twitter-entities/1351347418332278785/EsDzUBtXIAANJUQ.jpg
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
twitter-entities/1351349705071087616/EsD1ZOBXAAM3N4L.jpg
Normal file
After Width: | Height: | Size: 288 KiB |
BIN
twitter-entities/1351352097179107330/EsD3kb1XUAU0Awa.jpg
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
twitter-entities/1351354176178188288/EsD5dbsXAAE09up.jpg
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
twitter-entities/1351355625779961858/EsD6x0UXAAYTGpw.jpg
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
twitter-entities/1351355676992290816/EsD6QkHVcAAEUIN.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
twitter-entities/1351358737513123844/EsD9m5JXAAMxDRR.jpg
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
twitter-entities/1351361678257041410/EsEASKzXEAUqrPV.jpg
Normal file
After Width: | Height: | Size: 96 KiB |