From 3e7703c57327397ca821d5eca26c4be8c52e36d1 Mon Sep 17 00:00:00 2001 From: syui Date: Sat, 13 Apr 2024 02:05:05 +0900 Subject: [PATCH] add bot custom feed --- .config/ai/scpt | 2 +- at/feed-generator/env | 10 + at/feed-generator/readme.md | 20 ++ at/feed-generator/src/algos/cmd.ts | 43 +++ at/feed-generator/src/algos/index.ts | 14 + at/feed-generator/src/subscription.ts | 51 ++++ src/bot.rs | 424 ++++++++++++++++++++++++++ src/data.rs | 3 + src/feed_get.rs | 33 ++ src/main.rs | 29 ++ 10 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 at/feed-generator/env create mode 100644 at/feed-generator/readme.md create mode 100644 at/feed-generator/src/algos/cmd.ts create mode 100644 at/feed-generator/src/algos/index.ts create mode 100644 at/feed-generator/src/subscription.ts create mode 100644 src/feed_get.rs diff --git a/.config/ai/scpt b/.config/ai/scpt index b16ed58..70595e1 160000 --- a/.config/ai/scpt +++ b/.config/ai/scpt @@ -1 +1 @@ -Subproject commit b16ed585525aa279c1caeae606b8e49cc6589c7a +Subproject commit 70595e19511e7970ecdbe5fe556eef9380a9fbdf diff --git a/at/feed-generator/env b/at/feed-generator/env new file mode 100644 index 0000000..7b86bb4 --- /dev/null +++ b/at/feed-generator/env @@ -0,0 +1,10 @@ +FEEDGEN_PORT=3000 +FEEDGEN_LISTENHOST="0.0.0.0" +FEEDGEN_SQLITE_LOCATION="/data/db.sqlite" +FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bgs.syu.is" +FEEDGEN_PUBLISHER_DID="did:web:feed.syu.is" +FEEDGEN_HOSTNAME="feed.syu.is" + +FEEDGEN_SUBSCRIPTION_RECONNECT_DELAY=3000 +FEEDGEN_PUBLISHER_DID=did:plc:4hqjfn7m6n5hno3doamuhgef +FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bsky.network" diff --git a/at/feed-generator/readme.md b/at/feed-generator/readme.md new file mode 100644 index 0000000..17854e5 --- /dev/null +++ b/at/feed-generator/readme.md @@ -0,0 +1,20 @@ +# custom feed + +- at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd +- [bsky.app](https://bsky.app/profile/did:plc:4hqjfn7m6n5hno3doamuhgef/feed/cmd) +- [app.bsky.feed.getFeedSkeleton](https://feed.syu.is/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd) + +```sh +did=did:plc:4hqjfn7m6n5hno3doamuhgef +col=app.bsky.feed.generator +cid=cmd +uri=at://$did/$col/$cid + +echo $uri +``` + +## bsky-feed + +```sh +$ git clone https://github.com/bluesky-social/feed-generator +``` diff --git a/at/feed-generator/src/algos/cmd.ts b/at/feed-generator/src/algos/cmd.ts new file mode 100644 index 0000000..bac1f4a --- /dev/null +++ b/at/feed-generator/src/algos/cmd.ts @@ -0,0 +1,43 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' +import { AppContext } from '../config' + +// max 15 chars +export const shortname = 'cmd' + +export const handler = async (ctx: AppContext, params: QueryParams) => { + let builder = ctx.db + .selectFrom('post') + .selectAll() + .orderBy('indexedAt', 'desc') + .orderBy('cid', 'desc') + .limit(params.limit) + + if (params.cursor) { + const [indexedAt, cid] = params.cursor.split('::') + if (!indexedAt || !cid) { + throw new InvalidRequestError('malformed cursor') + } + const timeStr = new Date(parseInt(indexedAt, 10)).toISOString() + builder = builder + .where('post.indexedAt', '<', timeStr) + .orWhere((qb) => qb.where('post.indexedAt', '=', timeStr)) + .where('post.cid', '<', cid) + } + const res = await builder.execute() + + const feed = res.map((row) => ({ + post: row.uri, + })) + + let cursor: string | undefined + const last = res.at(-1) + if (last) { + cursor = `${new Date(last.indexedAt).getTime()}::${last.cid}` + } + + return { + cursor, + feed, + } +} diff --git a/at/feed-generator/src/algos/index.ts b/at/feed-generator/src/algos/index.ts new file mode 100644 index 0000000..4490a2d --- /dev/null +++ b/at/feed-generator/src/algos/index.ts @@ -0,0 +1,14 @@ +import { AppContext } from '../config' +import { + QueryParams, + OutputSchema as AlgoOutput, +} from '../lexicon/types/app/bsky/feed/getFeedSkeleton' +import * as cmd from './cmd' + +type AlgoHandler = (ctx: AppContext, params: QueryParams) => Promise + +const algos: Record = { + [cmd.shortname]: cmd.handler, +} + +export default algos diff --git a/at/feed-generator/src/subscription.ts b/at/feed-generator/src/subscription.ts new file mode 100644 index 0000000..bfec084 --- /dev/null +++ b/at/feed-generator/src/subscription.ts @@ -0,0 +1,51 @@ +import { + OutputSchema as RepoEvent, + isCommit, +} from './lexicon/types/com/atproto/sync/subscribeRepos' +import { FirehoseSubscriptionBase, getOpsByType } from './util/subscription' + +export class FirehoseSubscription extends FirehoseSubscriptionBase { + async handleEvent(evt: RepoEvent) { + if (!isCommit(evt)) return + const ops = await getOpsByType(evt) + + // This logs the text of every post off the firehose. + // Just for fun :) + // Delete before actually using + for (const post of ops.posts.creates) { + console.log(post.record.text) + } + + const postsToDelete = ops.posts.deletes.map((del) => del.uri) + const postsToCreate = ops.posts.creates + .filter((create) => { + // only alf-related posts + return create.record.text.match('^/[a-z]'); + //return create.record.text.toLowerCase().includes('alf') + }) + .map((create) => { + // map alf-related posts to a db row + return { + uri: create.uri, + cid: create.cid, + replyParent: create.record?.reply?.parent.uri ?? null, + replyRoot: create.record?.reply?.root.uri ?? null, + indexedAt: new Date().toISOString(), + } + }) + + if (postsToDelete.length > 0) { + await this.db + .deleteFrom('post') + .where('uri', 'in', postsToDelete) + .execute() + } + if (postsToCreate.length > 0) { + await this.db + .insertInto('post') + .values(postsToCreate) + .onConflict((oc) => oc.doNothing()) + .execute() + } + } +} diff --git a/src/bot.rs b/src/bot.rs index 354fd15..f90089b 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -7,6 +7,7 @@ use crate::openai; use crate::refresh; use crate::reply; use crate::reply_link; +use crate::feed_get; use crate::data::c_char; use crate::data::data_scpt; @@ -14,6 +15,7 @@ use crate::data::data_toml; use crate::data::log_file; use crate::data::w_cid; use crate::data::Notify; +use crate::data::Timeline; pub fn c_bot(c: &Context) { let h = async { @@ -508,3 +510,425 @@ pub fn c_bot(c: &Context) { let res = tokio::runtime::Runtime::new().unwrap().block_on(h); return res; } + +pub fn c_bot_feed(c: &Context) { + let mut feed = "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd".to_string(); + if c.string_flag("feed").is_ok() { + feed = c.string_flag("feed").unwrap(); + } + let h = async { + let mut notify = feed_get::get_request(feed).await; + if notify == "err" { + refresh(c); + notify = feed_get::get_request("at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd".to_string()).await; + } + let timeline: Timeline = serde_json::from_str(¬ify).unwrap(); + let n = timeline.feed; + let host = data_toml(&"host"); + let length = &n.len(); + let su = 0..*length; + for i in su { + let handle = &n[i].post.author.handle; + let did = &n[i].post.author.did; + let cid = &n[i].post.cid; + let uri = &n[i].post.uri; + let _time = &n[i].post.indexedAt; + let cid_root = cid; + let uri_root = uri; + let check_cid = w_cid(cid.to_string(), log_file(&"n1"), false); + let check_cid_run = w_cid(cid.to_string(), log_file(&"n2"), false); + + let mut text = ""; + if !n[i].post.record.text.is_none() { + text = &n[i].post.record.text.as_ref().unwrap(); + } + let vec: Vec<&str> = text.split_whitespace().collect(); + let handlev: Vec<&str> = handle.split('.').collect(); + let mut handlev = handlev[0].trim().to_string(); + let mut ten_p = "false"; + + let mut link = "https://card.syui.ai/".to_owned() + &handlev; + let s = 0; + let mut e = link.chars().count(); + + let mut prompt_sub = "".to_string(); + let com = vec[0].trim().to_string(); + let prompt = vec[1..].join(" "); + if vec.len() > 1 { + prompt_sub = vec[2..].join(" "); + } + + if prompt.is_empty() == false || com.is_empty() == false { + println!("{}", handle); + println!( + "cid:{}\nuri:{}\ncid_root:{}\nuri_root:{}\nhost:{}", + cid, uri, cid_root, uri_root, host + ); + println!("prompt_sub:{}", prompt_sub); + } + + let mut admin = "".to_string(); + if c.string_flag("admin").is_ok() { + admin = c.string_flag("admin").unwrap(); + } + + if check_cid == false + || check_cid_run == false + { + w_cid(cid.to_string(), log_file(&"n2"), true); + if com == "did" { + let link = "https://plc.directory/".to_owned() + &did + &"/log"; + let s = 0; + let e = link.chars().count(); + let d = "\n".to_owned() + &did.to_string(); + let text_limit = c_char(d); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + w_cid(cid.to_string(), log_file(&"n1"), true); + println!("{}", str_rep); + } + } else if com == "help" || com == "/help" { + let link = "https://git.syui.ai/ai/bot/wiki/help".to_string(); + let s = 0; + let e = link.chars().count(); + let str_rep = reply_link::post_request( + "\n".to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + w_cid(cid.to_string(), log_file(&"n1"), true); + println!("{}", str_rep); + } else if { com == "diffusers" || com == "/diffusers" } && handle == &admin{ + let _output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"diffusers") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + w_cid(cid.to_string(), log_file(&"n1"), true); + } else if com.contains("占") == true + || com.contains("うらない") == true + || com.contains("うらなって") == true + { + let _output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"fortune") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + w_cid(cid.to_string(), log_file(&"n1"), true); + } else if com == "card" || com == "/card" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"card") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + if text_limit.len() > 3 { + //handlev = handle.replace(".", "-").to_string(); + handlev = d.lines().collect::>()[0].to_string(); + link = "https://card.syui.ai/".to_owned() + &handlev; + e = link.chars().count(); + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "fav" || com == "/fav" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"fav") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + if text_limit.len() > 3 { + handlev = d.lines().collect::>()[0].to_string(); + link = "https://card.syui.ai/".to_owned() + &handlev; + e = link.chars().count(); + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "egg" || com == "/egg" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"egg") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + if text_limit.len() > 3 { + handlev = d.lines().collect::>()[0].to_string(); + link = "https://card.syui.ai/".to_owned() + &handlev; + e = link.chars().count(); + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "nyan" || com == "/nyan" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"nyan") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + println!("{}", text_limit); + if text_limit.len() > 3 { + let str_rep = reply::post_request( + text_limit.to_string(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if com == "ten" || com == "/ten" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"ten") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + handlev = d.lines().collect::>()[0].to_string(); + let ten_l = d.lines().collect::>().len(); + println!("handlev {}", handlev); + println!("ten_l {}", ten_l); + if ten_l == 3 { + ten_p = d.lines().collect::>()[1]; + println!("ten_p {}", ten_p); + } + if ten_p != "true" { + link = "https://card.syui.ai/".to_owned() + &handlev; + e = link.chars().count(); + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + } + w_cid(cid.to_string(), log_file(&"n1"), true); + } else if com == "coin" || com == "/coin" { + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"coin") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let dd = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(dd); + handlev = d.lines().collect::>()[0].to_string(); + link = "https://card.syui.ai/".to_owned() + &handlev; + println!("{}", e); + e = link.chars().count(); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } else if { com == "sh" || com == "/sh" } && handle == &admin { + println!("admin:{}", admin); + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"sh") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = d.to_string(); + let text_limit = c_char(d); + let str_rep = reply::post_request( + text_limit.to_string(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } else if com == "mitractl" && handle == &admin { + println!("admin:{}", admin); + let output = Command::new(data_scpt(&"ai")) + .arg(&"atproto").arg(&"mitra") + .arg(&handle) + .arg(&did) + .arg(&cid) + .arg(&uri) + .arg(&cid_root) + .arg(&uri_root) + .arg(&host) + .arg(&prompt) + .arg(&prompt_sub) + .output() + .expect("zsh"); + let d = String::from_utf8_lossy(&output.stdout); + let d = "\n".to_owned() + &d.to_string(); + let text_limit = c_char(d); + link = "https://m.syu.is".to_string(); + e = link.chars().count(); + if text_limit.len() > 3 { + let str_rep = reply_link::post_request( + text_limit.to_string(), + link.to_string(), + s, + e.try_into().unwrap(), + cid.to_string(), + uri.to_string(), + cid_root.to_string(), + uri_root.to_string(), + ) + .await; + println!("{}", str_rep); + w_cid(cid.to_string(), log_file(&"n1"), true); + } + } + println!("---"); + } + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} diff --git a/src/data.rs b/src/data.rs index 3f569e6..540a36d 100644 --- a/src/data.rs +++ b/src/data.rs @@ -123,6 +123,7 @@ pub struct BaseUrl { pub follow: String, pub follows: String, pub followers: String, + pub feed_get: String, } pub fn url(s: &str) -> String { @@ -148,6 +149,7 @@ pub fn url(s: &str) -> String { session_refresh: "com.atproto.server.refreshSession".to_string(), session_get: "com.atproto.server.getSession".to_string(), timeline_get: "app.bsky.feed.getTimeline".to_string(), + feed_get: "app.bsky.feed.getFeed".to_string(), timeline_author: "app.bsky.feed.getAuthorFeed".to_string(), like: "app.bsky.feed.like".to_string(), repost: "app.bsky.feed.repost".to_string(), @@ -187,6 +189,7 @@ pub fn url(s: &str) -> String { "follow" => t.to_string() + &baseurl.follow, "follows" => t.to_string() + &baseurl.follows, "followers" => t.to_string() + &baseurl.followers, + "feed_get" => t.to_string() + &baseurl.feed_get, _ => s, } } diff --git a/src/feed_get.rs b/src/feed_get.rs new file mode 100644 index 0000000..305868c --- /dev/null +++ b/src/feed_get.rs @@ -0,0 +1,33 @@ +extern crate reqwest; +use crate::data_refresh; +use crate::url; + +pub async fn get_request(feed: String) -> String { + let token = data_refresh(&"access"); + let url = url(&"feed_get"); + let feed = feed.to_string(); + //let col = "app.bsky.feed.generator".to_string(); + + let client = reqwest::Client::new(); + let res = client + .get(url) + .query(&[("feed", feed)]) + //.query(&[("feed", feed), ("collection", col)]) + .header("Authorization", "Bearer ".to_owned() + &token) + .send() + .await + .unwrap(); + + let status_ref = res.error_for_status_ref(); + + match status_ref { + Ok(_) => { + return res.text().await.unwrap(); + } + Err(_e) => { + let e = "err".to_string(); + return e; + } + } + +} diff --git a/src/main.rs b/src/main.rs index d403067..da4869d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::env; use crate::ascii::c_ascii; use crate::bot::c_bot; +use crate::bot::c_bot_feed; use crate::data::c_follow_all; use crate::data::c_openai_key; use crate::data::data_toml; @@ -37,6 +38,7 @@ pub mod repost; pub mod session; pub mod timeline_author; pub mod token; +pub mod feed_get; fn main() { let args: Vec = env::args().collect(); @@ -59,6 +61,10 @@ fn main() { Flag::new("admin", FlagType::String) .alias("a"), ) + .flag( + Flag::new("feed", FlagType::String) + .alias("f"), + ) .flag( Flag::new("manga_uri", FlagType::String) ) @@ -98,6 +104,12 @@ fn main() { .alias("t") .action(timeline), ) + .command( + Command::new("feed") + .description("feed ") + .alias("f") + .action(feed) + ) .command( Command::new("did") .description("did ") @@ -274,6 +286,7 @@ fn bot(c: &Context) { refresh(c); loop { c_bot(c); + c_bot_feed(c); } } @@ -337,6 +350,22 @@ fn notify(c: &Context) { return res; } +fn feed(c: &Context) { + refresh(c); + let feed_d = "at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd".to_string(); + let h = async { + if c.args.len() == 0 { + let j = feed_get::get_request(feed_d).await; + println!("{}", j); + } else { + let j = feed_get::get_request(c.args[0].to_string()).await; + println!("{}", j); + } + }; + let res = tokio::runtime::Runtime::new().unwrap().block_on(h); + return res; +} + fn did(c: &Context) { refresh(c); let h = async {