1
0

add bot custom feed
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s

This commit is contained in:
syui 2024-04-13 02:05:05 +09:00
parent 5d60645c0f
commit 92452e106b
Signed by: syui
GPG Key ID: 5417CFEBAD92DF56
11 changed files with 710 additions and 1 deletions

@ -1 +1 @@
Subproject commit b16ed585525aa279c1caeae606b8e49cc6589c7a
Subproject commit 70595e19511e7970ecdbe5fe556eef9380a9fbdf

10
at/feed-generator/env Normal file
View File

@ -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"

View File

@ -0,0 +1,26 @@
# 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
```
```sh
docker compose build feed-generator
docker build -t publish_feed -f Dockerfile .
docker run publish_feed
```

View File

@ -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,
}
}

View File

@ -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<AlgoOutput>
const algos: Record<string, AlgoHandler> = {
[cmd.shortname]: cmd.handler,
}
export default algos

View File

@ -0,0 +1,50 @@
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) => {
return create.record.text.match('^/[a-z]') || create.record.text.match('^@ai ') || create.record.text.match('^ai ');
//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()
}
}
}

View File

@ -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,445 @@ 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(&notify).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 com = vec[0].trim().to_string();
let mut prompt = "".to_string();
let mut prompt_sub = "".to_string();
let mut prompt_chat = "".to_string();
if com == "@ai" {
prompt_chat = vec[1..].join(" ");
} else {
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::<Vec<_>>()[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::<Vec<_>>()[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::<Vec<_>>()[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::<Vec<_>>()[0].to_string();
let ten_l = d.lines().collect::<Vec<_>>().len();
println!("handlev {}", handlev);
println!("ten_l {}", ten_l);
if ten_l == 3 {
ten_p = d.lines().collect::<Vec<_>>()[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::<Vec<_>>()[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);
}
} else if com == "@ai" {
let str_openai = openai::post_request(prompt_chat.to_string()).await;
let text_limit = c_char(str_openai);
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);
}
println!("---");
}
}
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res;
}

View File

@ -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,
}
}

33
src/delete_record.rs Normal file
View File

@ -0,0 +1,33 @@
extern crate reqwest;
use crate::data_toml;
use crate::data_refresh;
use crate::url;
use serde_json::json;
pub async fn post_request(rkey: String, col: String) -> String {
let token = data_refresh(&"access");
//let did = data_toml(&"did");
let handle = data_toml(&"handle");
let url = url(&"record_delete");
let post = Some(json!({
"repo": handle.to_string(),
"rkey": rkey.to_string(),
"collection": col.to_string()
}));
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res;
}

33
src/feed_get.rs Normal file
View File

@ -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;
}
}
}

View File

@ -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,8 @@ pub mod repost;
pub mod session;
pub mod timeline_author;
pub mod token;
pub mod feed_get;
pub mod delete_record;
fn main() {
let args: Vec<String> = env::args().collect();
@ -59,6 +62,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 +105,12 @@ fn main() {
.alias("t")
.action(timeline),
)
.command(
Command::new("feed")
.description("feed <feed-uri>")
.alias("f")
.action(feed)
)
.command(
Command::new("did")
.description("did <handle>")
@ -113,6 +126,16 @@ fn main() {
.alias("l"),
)
)
.command(
Command::new("delete")
.description("d <rkey>")
.alias("d")
.action(delete)
.flag(
Flag::new("col", FlagType::String)
.alias("c"),
)
)
.command(
Command::new("like")
.description("like <cid> -u <uri>")
@ -274,6 +297,7 @@ fn bot(c: &Context) {
refresh(c);
loop {
c_bot(c);
c_bot_feed(c);
}
}
@ -337,6 +361,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 {
@ -386,6 +426,19 @@ fn post(c: &Context) {
return res;
}
fn delete(c: &Context) {
refresh(c);
let m = c.args[0].to_string();
let h = async {
if let Ok(col) = c.string_flag("col") {
let str = delete_record::post_request(m.to_string(), col);
println!("{}", str.await);
}
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res;
}
fn like(c: &Context) {
refresh(c);
let m = c.args[0].to_string();